Skip to content

metrics: bound ate.sandbox.class with one rule for every emitter - #1484

Merged
Krisztian F (krisztianfekete) merged 4 commits into
agent-substrate:mainfrom
JeffLuoo:fix/issue-1475-workerpool-workers-sandbox-class
Sep 9, 2026
Merged

metrics: bound ate.sandbox.class with one rule for every emitter#1484
Krisztian F (krisztianfekete) merged 4 commits into
agent-substrate:mainfrom
JeffLuoo:fix/issue-1475-workerpool-workers-sandbox-class

Conversation

@JeffLuoo

@JeffLuoo Jeff Luo (JeffLuoo) commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1475

Control.CreateWorker does not validate Worker.sandbox_class, so a client can register a worker with an empty one (and any record written before this keeps it). RegisterWorkerCount tallied that raw value, so ate.workerpool.workers emitted ate_sandbox_class="" — not a member of the ate.sandbox.class registry vocabulary — alongside the pool's seeded series at 0.

This change normalizes the worker's class with the existing ateattr.NormalizeSandboxClass, so an empty class reports as unknown.

New ateattr.SandboxClassAttribute, used by every emitter. This closes the same gap on ate.actor.crashes and the ate.actor.lifecycle.*.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Actually, before making this change I have a question about the scheduling - Julian Gutierrez Oschmann (@juli4n)

Worker.sandbox_class is optional:

// sandbox_class mirrors the WorkerPool's sandboxClass; its values are the
// CRD's own vocabulary, so it is only bounded, not validated. Mutable.
//
// +k8s:optional
// +k8s:maxLength=63
string sandbox_class = 8;

and it doesn't have a default value. But this field will be used by scheduler to schedule the worker. CreateWorker accepts "" and nothing fills it in. Should we validate it from the API level?

@JeffLuoo
Jeff Luo (JeffLuoo) force-pushed the fix/issue-1475-workerpool-workers-sandbox-class branch from 9bd647a to 365c075 Compare September 4, 2026 19:18
@JeffLuoo Jeff Luo (JeffLuoo) changed the title metrics: default empty SandboxClass in syncer and workerpool metrics metrics: report a worker with no sandbox class as unknown Sep 4, 2026
@BenTheElder

Copy link
Copy Markdown
Collaborator

So, I also want to mention that I think workers will have multiple sandbox classes in the future. Actors will be on a specific sandbox class but the idea that we reserve compute only for gvisor or microVM is an artificial distinction that probably won't hold up as we incrementally improve the dataplane.

would that suggest a different approach here?

@JeffLuoo

Jeff Luo (JeffLuoo) commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

So, I also want to mention that I think workers will have multiple sandbox classes in the future. Actors will be on a specific sandbox class but the idea that we reserve compute only for gvisor or microVM is an artificial distinction that probably won't hold up as we incrementally improve the dataplane.

would that suggest a different approach here?

Thank you for the context. I patch the PR to drop the change in earlier revision had an empty-class worker inherit its pool's class. This PR only normalizes empty string sandbox class to unknown like other metrics.

For the impact of "workers will have multiple sandbox classes in the future" to the metric ate.workerpool.workers, the sum across sandbox classes stops equaling the fleet size. My guess is that worker counts lose the class label?

cc Krisztian F (@krisztianfekete) we may want to revisit some metrics when this happens.

@JeffLuoo
Jeff Luo (JeffLuoo) marked this pull request as ready for review September 4, 2026 19:48

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach looks right to me, two things:

  1. The same empty class is still on ate.actor.crashes.
    Same bug as #1475, on an attribute the registry marks required. Maybe we should have a tiny SandboxClassAttribute helper in
    ateattr so every emitter shares one rule. Fine as a follow up, but please file it
    from this PR so it doesn't close along with #1475.

  2. sum by (pool) (ate_workerpool_workers{state="idle"}) still returns 3 for that
    pool, because the unknown series keeps the pool labels. Not a regression, as ""
    counted the same way, but as per #1475 these should not not count as pool capacity, and that only happoens if the query includes class.

@krisztianfekete

Copy link
Copy Markdown
Collaborator

So, I also want to mention that I think workers will have multiple sandbox classes in the future. Actors will be on a specific sandbox class but the idea that we reserve compute only for gvisor or microVM is an artificial distinction that probably won't hold up as we incrementally improve the dataplane.

would that suggest a different approach here?

I don't think it changes this PR. We should still do this regardless of how many classes a worker ends up having.

For the impact of "workers will have multiple sandbox classes in the future" to the metric ate.workerpool.workers, the sum across sandbox classes stops equaling the fleet size. My guess is that worker counts lose the class label?

Today, the instrument's whole contract is that the counts are summable, so sum by (pool) = pool size and sum over pools = fleet size, which is why it's an UpDownCounter and not a gauge. Once a worker has a classes, a per-class dimension will double count it. If we drop the class label all is correct, but we cannot answer the "capacity per kind" question, which is something we'd like to answer for sure.

So I'd expect it to split, so worker counts stay class-less and summable, and maybe eligibility-by-class becomes its own instrument, where overlaps are fine, and no one wants to sum them. That's ~what ate.scheduler.eligible_workers already is.

Not worth designing now while the dataplane is still evolving, and I'd land the label fix first and revisit when the multi-class model becomes stable. Do you mind opening on issue for this?

Control.CreateWorker does not validate Worker.sandbox_class, so a client can
register a worker carrying an empty one, and any worker written before this
keeps it. RegisterWorkerCount tallied that raw value, emitting
ate.workerpool.workers with ate.sandbox.class="" -- not a member of the
attribute's registry vocabulary -- next to the pool's seeded series at 0.

Normalize the worker's class with ateattr.NormalizeSandboxClass, so an empty
class reports as unknown. That is the rule the same helper already applies to
this attribute on the atelet side, so the codebase keeps one rule rather than
two.

The worker is deliberately not folded into the series of the pool it names.
Scheduling matches sandbox_class exactly, so a worker with no class can host no
actor; counting it as pool capacity would inflate the idle state and silence an
idle == 0 alert while every resume fails for want of a worker. Under unknown it
stays visible and alertable, and the pool's own counts stay truthful.

The empty class enters through the worker record, not the WorkerPool CR: the
CRD defaults sandboxClass to gvisor and rejects "" by enum. Validating
Worker.sandbox_class at the API boundary would stop the empty state existing at
all; that is a scheduling fix, and a separate change.
The class reaches the emitters from sources that nothing validates, and each
emitter decided for itself what to do with a value outside the attribute's
vocabulary. Only atelet normalized. So ate.actor.crashes and the
ate.actor.lifecycle timers could carry ate.sandbox.class="", which the registry
marks required and permits only gvisor, microvm and unknown.

The crash counter has a source of its own, separate from the unvalidated worker
record: releaseWorker returns an empty class when the worker is already gone or
its assignment is already cleared. There the empty means the class could not be
read, which is what unknown is for.

Add ateattr.SandboxClassAttribute and stamp the attribute through it, so the
rule lives in one place. recordSchedulerAssignment keeps its own behavior: it
leaves the attribute off when the class is unknown, which its comment explains,
and that is a deliberate choice rather than a missing bound.
@JeffLuoo
Jeff Luo (JeffLuoo) force-pushed the fix/issue-1475-workerpool-workers-sandbox-class branch from 66ba5a5 to 7ad23ee Compare September 8, 2026 14:45
@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Approach looks right to me, two things:

  1. The same empty class is still on ate.actor.crashes.
    Same bug as ate.workerpool.workers emits a empty string series with ate_sandbox_class="" #1475, on an attribute the registry marks required. Maybe we should have a tiny SandboxClassAttribute helper in
    ateattr so every emitter shares one rule. Fine as a follow up, but please file it
    from this PR so it doesn't close along with ate.workerpool.workers emits a empty string series with ate_sandbox_class="" #1475.
  2. sum by (pool) (ate_workerpool_workers{state="idle"}) still returns 3 for that
    pool, because the unknown series keeps the pool labels. Not a regression, as ""
    counted the same way, but as per ate.workerpool.workers emits a empty string series with ate_sandbox_class="" #1475 these should not not count as pool capacity, and that only happoens if the query includes class.

For point 1, the change is not too long so I just do it in this PR.

@JeffLuoo Jeff Luo (JeffLuoo) changed the title metrics: report a worker with no sandbox class as unknown metrics: bound ate.sandbox.class with one rule for every emitter Sep 8, 2026
Comment thread internal/ateattr/ateattr.go Outdated
Comment thread cmd/ateapi/internal/controlapi/metrics_test.go Outdated
Comment thread cmd/ateapi/internal/controlapi/metrics_test.go Outdated
Comment thread internal/ateattr/ateattr.go
…mment

Review fixes.

recordEligibleWorkers set ate.sandbox.class from the raw Constraints value. The
class comes from sandboxClassString, which gives an empty string for an
unspecified enum, thus the same gap the helper closes elsewhere. The attribute
is required on ate.scheduler.eligible_workers.

Its test asked for the class "kata" to prove a mismatch. That value cannot
reach the scheduler: sandboxClassString gives only gvisor, microvm or empty.
The test now asks for microvm against a gvisor fleet, which is the same
mismatch with a class that the vocabulary permits.

The helper's comment named one omit-site. snapshotOp.attrs is a second, and its
own comment says so.

Also fold the three worker-count tests into a table, and take the class values
from the API constants.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest looks good!

Only thing left is docs/, still untouched in the PR. Three instruments produce
unknown now instead of two, so metrics.yaml also need to be updated, + the two lines on the workers note about unknown not being capacity.

Could you also open the multi-class followup so for Ben's thread?

@JeffLuoo

Jeff Luo (JeffLuoo) commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Created #1557

Only thing left is docs/, still untouched in the PR. Three instruments produce
unknown now instead of two, so metrics.yaml also need to be updated, + the two lines on the workers note about unknown not being capacity.

Updated

Four ateapi instruments can now report unknown: the worker count, the crash
counter, the lifecycle timers and the eligible-worker histogram. The member's
brief described only the atelet source, thus it now names each source.

The worker count needs two more lines of its own. A worker with the class
unknown is not capacity, thus a query that adds the classes together reports
capacity that no resume can use.
@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Krisztian F (@krisztianfekete) can you help merge? Not suppose to be merged by author : )

@krisztianfekete
Krisztian F (krisztianfekete) merged commit 9ad967a into agent-substrate:main Sep 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ate.workerpool.workers emits a empty string series with ate_sandbox_class=""

3 participants